/*
 * mbport_serial.h
 *
 * Change Logs:
 * Date           Author       Notes
 * 2022-11-09     nx      	   the first version
 *
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <errno.h>
#include "mbport_serial.h"
#include "modbus_utils.h"

#define MB_SERIAL_LOCK(mutex)    pthread_mutex_lock(mutex)
#define MB_SERIAL_UNLOCK(mutex)  pthread_mutex_unlock(mutex)

#define MB_SERIAL_MAX_NUM       4

static mbport_serial_t mb_serial_tab[MB_SERIAL_MAX_NUM];

mbport_serial_t *_mb_serial_find(mb_uint8_t port_num)
{
    mb_uint8_t idx;
    for (idx = 0; idx < MB_SERIAL_MAX_NUM; idx++)
    {
        if ((mb_serial_tab[idx].magic == 0x1A2B) &&
            (mb_serial_tab[idx].port == port_num))
        {
            return &mb_serial_tab[idx];
        }
    }
    return MB_NULL;
}

mbport_serial_t *_mb_serial_get(void)
{
    mb_uint8_t idx;
    for (idx = 0; idx < MB_SERIAL_MAX_NUM; idx++)
    {
        if (mb_serial_tab[idx].magic != 0x1A2B)
        {
            // pthread_mutexattr_init(&mb_serial_tab[idx].mutexattr);
            // pthread_mutexattr_settype(&mb_serial_tab[idx].mutexattr, PTHREAD_MUTEX_RECURSIVE_NP);
            if (pthread_mutex_init(&mb_serial_tab[idx].mutex, NULL) == 0)
            {
                mb_serial_tab[idx].magic = 0x1A2B;
                return &mb_serial_tab[idx];
            } 
            else
            {
                mb_serial_tab[idx].magic = 0;
                return MB_NULL;
            }
        }
    }
    return MB_NULL;
}

void _mb_serial_put(mbport_serial_t *mb_serial)
{
    pthread_mutex_destroy(&mb_serial->mutex);
    pthread_mutexattr_destroy(&mb_serial->mutexattr);
    mb_serial->magic = 0;
    mb_serial->handle = -1;
    mb_serial->port = 0;
}

static mb_int32_t _mbport_serial_open(modbus_port_t *mb_port)
{
    char name[20] = {0};
    mbport_serial_t *mb_serial = (mbport_serial_t *)mb_port;

    //sprintf(name, "/dev/ttymxc%d", mb_serial->port - 1);
    sprintf(name, "/dev/ttyUSB%d", mb_serial->port - 1);
    
    mb_serial->handle = open(name, O_RDWR | O_NOCTTY | O_NDELAY);

    if (mb_serial->handle < 0)
    {
        MODBUS_DBG("Open %s fail, %s\n", name, strerror(errno));
        return -MB_EIO;
    }

    if (tcsetattr(mb_serial->handle, TCSANOW, &mb_serial->tios) < 0)
    {
        close(mb_serial->handle);
        mb_serial->handle = -1;
        MODBUS_DBG("%s set attribute fail.\n", name);
        return -MB_EIO;
    }
    // MODBUS_DBG("Open %s success.\n", name);

    return MB_ENOERR;
}

static mb_int32_t _mbport_serial_close(modbus_port_t *mb_port)
{
    mb_int32_t ret;
    mbport_serial_t *mb_serial = (mbport_serial_t *)mb_port;

    if (close(mb_serial->handle) < 0)
    {
        MODBUS_DBG("Close port %d fail.\n", mb_serial->port - 1);
        ret = -MB_EIO;
    }
    else
    {
        //MODBUS_DBG("Close %s success.\n", mb_serial->name);
        mb_serial->handle = -1;
        ret = MB_ENOERR;
    }

    return ret;
}

static mb_int32_t _mbport_serial_send(modbus_port_t *mb_port,
    mb_uint8_t *frame, mb_uint16_t length, mb_uint32_t timeout)
{
    mb_int32_t n_bytes;
    mbport_serial_t *mb_serial = (mbport_serial_t *)mb_port;

    n_bytes = write(mb_serial->handle, frame, (size_t)length);
    return n_bytes;
}

static mb_int32_t _mbport_serial_recv(modbus_port_t *mb_port,
    mb_uint8_t *frame, mb_uint16_t length, mb_uint32_t timeout)
{
    int rc;
    mb_int32_t ret;
    mb_uint16_t len;
    fd_set read_fds;
    struct timeval timval;
    mbport_serial_t *mb_serial = (mbport_serial_t *)mb_port;

    if (frame == NULL)
    {
        ret = 0;
        goto err_exit;
    }

    len = 0;
    FD_ZERO(&read_fds);
    FD_SET(mb_serial->handle, &read_fds);

    timval.tv_sec = timeout / 1000;
    timval.tv_usec = (timeout % 1000) * 1000;

    rc = select(mb_serial->handle + 1, &read_fds, NULL, NULL, &timval);
    if (rc <= 0)
    {
        ret = 0;
        goto err_exit;
    }
    
    if (read(mb_serial->handle, frame, 1) != 1)
    {
        /* Some kind of error */
        ret = 0;
        goto err_exit;
    }

    len = 1;
    timeout = 100;
    timval.tv_sec = timeout / 1000;
    timval.tv_usec = (timeout % 1000) * 1000;

    while (len < length)
    {
        rc = select(mb_serial->handle + 1, &read_fds, NULL, NULL, &timval);
        if (rc <= 0)
        {
            break;
        }
        rc = read(mb_serial->handle, &frame[len], (size_t)(length - len));
        if (rc < 0)
        {
            /* Some kind of error */
            break;
        }
        len += rc;
    }

    ret = len;

err_exit:
    return ret;
}

static mb_int32_t _mbport_serial_control(modbus_port_t *mb_port, mb_uint32_t cmd, void *args)
{
    long lock;
    speed_t speed;
    mbport_sercfg_t *sercfg;
    mbport_serial_t *mb_serial = (mbport_serial_t *)mb_port;

    if (cmd == MODBUS_CTRL_READ)
    {

    }
    else if (cmd == MODBUS_CTRL_WRITE)
    {
        sercfg = (mbport_sercfg_t *)args;
        modbus_memset(&mb_serial->tios, 0, sizeof(struct termios));

        /* C_ISPEED     Input baud (new interface)
        C_OSPEED     Output baud (new interface)
        */
        switch (sercfg->baud) 
        {
        case 110:
            speed = B110;
            break;
        case 300:
            speed = B300;
            break;
        case 600:
            speed = B600;
            break;
        case 1200:
            speed = B1200;
            break;
        case 2400:
            speed = B2400;
            break;
        case 4800:
            speed = B4800;
            break;
        case 9600:
            speed = B9600;
            break;
        case 19200:
            speed = B19200;
            break;
        case 38400:
            speed = B38400;
            break;
#ifdef B57600
        case 57600:
            speed = B57600;
            break;
#endif
#ifdef B115200
        case 115200:
            speed = B115200;
            break;
#endif
#ifdef B230400
        case 230400:
            speed = B230400;
            break;
#endif
#ifdef B460800
        case 460800:
            speed = B460800;
            break;
#endif
#ifdef B500000
        case 500000:
            speed = B500000;
            break;
#endif
#ifdef B576000
        case 576000:
            speed = B576000;
            break;
#endif
#ifdef B921600
        case 921600:
            speed = B921600;
            break;
#endif
#ifdef B1000000
        case 1000000:
            speed = B1000000;
            break;
#endif
#ifdef B1152000
        case 1152000:
            speed = B1152000;
            break;
#endif
#ifdef B1500000
        case 1500000:
            speed = B1500000;
            break;
#endif
#ifdef B2500000
        case 2500000:
            speed = B2500000;
            break;
#endif
#ifdef B3000000
        case 3000000:
            speed = B3000000;
            break;
#endif
#ifdef B3500000
        case 3500000:
            speed = B3500000;
            break;
#endif
#ifdef B4000000
        case 4000000:
            speed = B4000000;
            break;
#endif
        default:
            speed = B9600;
            MODBUS_DBG("WARNING Unknown baud rate %d for port %d (B9600 used)\n",
                        sercfg->baud, mb_serial->port);
        }
    

        /* Set the baud rate */
        if ((cfsetispeed(&mb_serial->tios, speed) < 0) ||
            (cfsetospeed(&mb_serial->tios, speed) < 0)) 
        {
            return -MB_EIO;
        }

        /* C_CFLAG      Control options
        CLOCAL       Local line - do not change "owner" of port
        CREAD        Enable receiver
        */
        mb_serial->tios.c_cflag |= (CREAD | CLOCAL);
        /* CSIZE, HUPCL, CRTSCTS (hardware flow control) */

        /* Set data bits (5, 6, 7, 8 bits)
        CSIZE        Bit mask for data bits
        */
        mb_serial->tios.c_cflag &= ~CSIZE;
        switch (sercfg->data_bit) 
        {
        case 5:
            mb_serial->tios.c_cflag |= CS5;
            break;
        case 6:
            mb_serial->tios.c_cflag |= CS6;
            break;
        case 7:
            mb_serial->tios.c_cflag |= CS7;
            break;
        case 8:
        default:
            mb_serial->tios.c_cflag |= CS8;
            break;
        }

        /* Stop bit (1 or 2) */
        if (sercfg->stop_bit == 1)
            mb_serial->tios.c_cflag &=~ CSTOPB;
        else /* 2 */
            mb_serial->tios.c_cflag |= CSTOPB;

        /* PARENB       Enable parity bit
        PARODD       Use odd parity instead of even */
        if (sercfg->parity == 0)
        {
            /* None */
            mb_serial->tios.c_cflag &=~ PARENB;
        }
        else if (sercfg->parity == 1)
        {
            /* Odd */
            mb_serial->tios.c_cflag |= PARENB;
            mb_serial->tios.c_cflag |= PARODD;
        }
        else if (sercfg->parity == 2)
        {
            /* Even */
            mb_serial->tios.c_cflag |= PARENB;
            mb_serial->tios.c_cflag &=~ PARODD;
        }
        
        /* Raw input */
        mb_serial->tios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

        if (sercfg->parity == 0)
        {
            /* None */
            mb_serial->tios.c_iflag &= ~INPCK;
        } else {
            mb_serial->tios.c_iflag |= INPCK;
        }

        /* Software flow control is disabled */
        mb_serial->tios.c_iflag &= ~(IXON | IXOFF | IXANY);

        /* C_OFLAG      Output options
        OPOST        Postprocess output (not set = raw output)
        ONLCR        Map NL to CR-NL

        ONCLR ant others needs OPOST to be enabled
        */

        /* Raw ouput */
        mb_serial->tios.c_oflag &=~ OPOST;

        /* Unused because we use open with the NDELAY option */
        mb_serial->tios.c_cc[VMIN] = 0;
        mb_serial->tios.c_cc[VTIME] = 0;

        if (mb_serial->handle > 0)
        {
            if (tcsetattr(mb_serial->handle, TCSANOW, &mb_serial->tios) < 0)
            {
                MODBUS_DBG("port %d set attribute fail.\n", mb_serial->port - 1);
                return -MB_EIO;
            }
        }
    }
    else if (cmd == MODBUS_CTRL_LOCK)
    {
        lock = (long)args;
        if (lock != 0)
            MB_SERIAL_LOCK(&mb_serial->mutex);
        else
            MB_SERIAL_UNLOCK(&mb_serial->mutex);
    }

    return MB_ENOERR;
}


mbport_serial_t *mbport_serial_create(mb_uint8_t port_num)
{
    mbport_serial_t *mb_serial = MB_NULL;
    mbport_sercfg_t mb_sercfg = SERIAL_CONFIG_DEFAULT;

    if (port_num == 0)
    {
        MODBUS_DBG("Port % is not supported.\n", port_num);
        return MB_NULL;
    }
    
    mb_serial = _mb_serial_find(port_num);
    if (mb_serial == NULL)
    {
         mb_serial = _mb_serial_get();
        if (mb_serial != MB_NULL)
        {
            _mbport_serial_control((modbus_port_t *)mb_serial, MODBUS_CTRL_WRITE, &mb_sercfg);
            mb_serial->port = port_num;
            mb_serial->parent.open_fn = _mbport_serial_open;
            mb_serial->parent.close_fn = _mbport_serial_close;
            mb_serial->parent.send_fn = _mbport_serial_send;
            mb_serial->parent.recv_fn = _mbport_serial_recv;
            mb_serial->parent.ctrl_fn = _mbport_serial_control;
        }
    }
   
    return mb_serial;
}
